package aws

import (
	"os"
	"io"
	"fmt"
	"time"
	"http"
	"xml"
	"sort"
	"strings"
)

type QueryRPC struct {
	Keys        AccessKeys
	APIVersion  string
	AuthVersion string
}

type nopCloser struct {
	io.Reader
}

func (nopCloser) Close() os.Error { return nil }

func (rpc *QueryRPC) GetResponse(rawurl, action string, params map[string]string) (resp *http.Response, err os.Error) {
	if params == nil {
		params = make(map[string]string)
	}
	params["Action"] = action
	params["Version"] = rpc.APIVersion

	if rpc.AuthVersion == "2" {
		params["AWSAccessKeyId"] = rpc.Keys.AccessKeyId
		params["Timestamp"] = time.UTC().Format(time.RFC3339)
		params["SignatureMethod"] = "HmacSHA256"
		params["SignatureVersion"] = "2"
	}

	// Construct query string
	names := make([]string, 0, len(params))
	for k, _ := range params {
		names = append(names, k)
	}
	sort.SortStrings(names)
	queryParts := make([]string, 0, len(params))
	for _, name := range names {
		queryParts = append(queryParts, name+"="+URLEncode(params[name]))
	}
	query := strings.Join(queryParts, "&")

	// Calculate signature
	headers := make(map[string]string)
	method := "GET"
	if len(rawurl)+len(query) > 2000 {
		method = "POST"
		headers["Content-Type"] = "application/x-www-form-urlencoded"
	}
	if rpc.AuthVersion == "2" {
		url, _ := http.ParseURL(rawurl)
		stringToSign := strings.Join([]string{method, strings.ToLower(url.Host), url.Path, query}, "\n")
		signature := CalculateSHA256Signature(rpc.Keys.SecretAccessKey, stringToSign)
		query += "&Signature=" + http.URLEscape(signature)
	} else {
		date := time.UTC().Format(http.TimeFormat)
		signature := CalculateSHA256Signature(rpc.Keys.SecretAccessKey, date)
		headers["Date"] = date
		headers["X-Amzn-Authorization"] = fmt.Sprintf("AWS3-HTTPS AWSAccessKeyId=%s, Algorithm=HmacSHA256, Signature=%s", rpc.Keys.AccessKeyId, signature)
	}

	for i, delay := 0, 4; i < 5; i, delay = i+1, delay*2 {
		url, err := http.ParseRequestURL(rawurl)
		if err != nil {
			return
		}
		req := &http.Request{
			Host:       url.Host,
			Method:     method,
			URL:        url,
			RawURL:     rawurl,
			ProtoMinor: 1,
			Close:      true,
			UserAgent:  "GoAWS"}
		req.Header = http.Header(make(http.Header))
		for k, v := range headers {
			req.Header.Set(k, v)
		}
		if method == "GET" {
			req.RawURL += "?" + query
		} else { // method == "POST"
			req.Body = nopCloser{strings.NewReader(query)}
			req.ContentLength = int64(len(query))
		}
		resp, err = http.DefaultTransport.Do(req)
		if err != nil {
			return nil, err
		}

		if resp.StatusCode != http.StatusInternalServerError && resp.StatusCode != http.StatusServiceUnavailable {
			break
		}
		time.Sleep(int64(delay * 1e8))
	}
	return
}

func (rpc *QueryRPC) UnmarshalXMLResponse(rawurl, action string, params map[string]string, successResp interface{}, newErrResp func() os.Error) (err os.Error) {
	resp, err := rpc.GetResponse(rawurl, action, params)
	if err != nil {
		return
	}

	if resp.StatusCode >= 300 {
		//io.Copy(os.Stderr, resp.Body)
		errResp := newErrResp()
		err = xml.Unmarshal(resp.Body, errResp)
		resp.Body.Close()
		if err != nil {
			return
		}

		return errResp
	}

	if successResp != nil {
		err = xml.Unmarshal(resp.Body, successResp)
		resp.Body.Close()
		if err != nil {
			return
		}
	} else { // successResp == nil
		name, err := GetFirstXMLElementName(resp.Body)
		resp.Body.Close()
		if err != nil {
			return err
		}

		if name.Local != action+"Response" {
			return os.NewError(fmt.Sprintf("Got unexpected response element %s (expected %s)", name.Local, action+"Response"))
		}
	}

	return
}
